using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using StardewModdingAPI.Toolkit.Serialization;

namespace StardewModdingAPI.Utilities;

/// <summary>A set of multi-key bindings which can be triggered by the player.</summary>
public class KeybindList
{
    /*********
    ** Accessors
    *********/
    /// <summary>The individual keybinds.</summary>
    public Keybind[] Keybinds { get; }

    /// <summary>Whether any keys are bound.</summary>
    public bool IsBound { get; }


    /*********
    ** Public methods
    *********/
    /// <summary>Construct an instance.</summary>
    /// <param name="keybinds">The underlying keybinds.</param>
    /// <remarks>See <see cref="Parse"/> or <see cref="TryParse"/> to parse it from a string representation. You can also use this type directly in your config or JSON data models, and it'll be parsed by SMAPI.</remarks>
    public KeybindList(params Keybind[] keybinds)
    {
        this.Keybinds = keybinds.Where(p => p.IsBound).ToArray();
        this.IsBound = this.Keybinds.Any();
    }

    /// <summary>Construct an instance.</summary>
    /// <param name="singleKey">A single-key binding.</param>
    public KeybindList(SButton singleKey)
        : this(new Keybind(singleKey)) { }

    /// <summary>Parse a keybind list from a string, and throw an exception if it's not valid.</summary>
    /// <param name="input">The keybind string. See remarks on <see cref="ToString"/> for format details.</param>
    /// <exception cref="FormatException">The <paramref name="input"/> format is invalid.</exception>
    public static KeybindList Parse(string input)
    {
        return KeybindList.TryParse(input, out KeybindList? parsed, out string[] errors)
            ? parsed
            : throw new SParseException($"Can't parse {nameof(Keybind)} from invalid value '{input}'.\n{string.Join("\n", errors)}");
    }

    /// <summary>Try to parse a keybind list from a string.</summary>
    /// <param name="input">The keybind string. See remarks on <see cref="ToString"/> for format details.</param>
    /// <param name="parsed">The parsed keybind list, if valid.</param>
    /// <param name="errors">The errors that occurred while parsing the input, if any.</param>
    public static bool TryParse(string? input, [NotNullWhen(true)] out KeybindList? parsed, out string[] errors)
    {
        // empty input
        if (string.IsNullOrWhiteSpace(input))
        {
            parsed = new KeybindList();
            errors = [];
            return true;
        }

        // parse buttons
        var rawErrors = new List<string>();
        var keybinds = new List<Keybind>();
        foreach (string rawSet in input.Split(','))
        {
            if (string.IsNullOrWhiteSpace(rawSet))
                continue;

            if (!Keybind.TryParse(rawSet, out Keybind? keybind, out string[] curErrors))
                rawErrors.AddRange(curErrors);
            else
                keybinds.Add(keybind);
        }

        // build result
        if (rawErrors.Any())
        {
            parsed = null;
            errors = rawErrors.Distinct().ToArray();
            return false;
        }
        else
        {
            parsed = new KeybindList(keybinds.ToArray());
            errors = [];
            return true;
        }
    }

    /// <summary>Get a keybind list for a single keybind.</summary>
    /// <param name="buttons">The buttons that must be down to activate the keybind.</param>
    public static KeybindList ForSingle(params SButton[] buttons)
    {
        return new KeybindList(
            new Keybind(buttons)
        );
    }

    /// <summary>Get the overall keybind list state relative to the previous tick.</summary>
    /// <remarks>States are transitive across keybind. For example, if one keybind is 'released' and another is 'pressed', the state of the keybind list is 'held'.</remarks>
    public SButtonState GetState()
    {
        bool wasPressed = false;
        bool isPressed = false;

        foreach (Keybind keybind in this.Keybinds)
        {
            switch (keybind.GetState())
            {
                case SButtonState.Pressed:
                    isPressed = true;
                    break;

                case SButtonState.Held:
                    wasPressed = true;
                    isPressed = true;
                    break;

                case SButtonState.Released:
                    wasPressed = true;
                    break;
            }
        }

        if (wasPressed == isPressed)
        {
            return wasPressed
                ? SButtonState.Held
                : SButtonState.None;
        }

        return wasPressed
            ? SButtonState.Released
            : SButtonState.Pressed;
    }

    /// <summary>Get whether any of the button sets are pressed.</summary>
    public bool IsDown()
    {
        SButtonState state = this.GetState();
        return state is SButtonState.Pressed or SButtonState.Held;
    }

    /// <summary>Get whether the input binding was just pressed this tick.</summary>
    public bool JustPressed()
    {
        return this.GetState() == SButtonState.Pressed;
    }

    /// <summary>Get the keybind which is currently down, if any. If there are multiple keybinds down, the first one is returned.</summary>
    public Keybind? GetKeybindCurrentlyDown()
    {
        return this.Keybinds.FirstOrDefault(p => p.GetState().IsDown());
    }

    /// <summary>Get a string representation of the input binding.</summary>
    /// <remarks>A keybind list is serialized to a string like <c>LeftControl + S, LeftAlt + S</c>, where each multi-key binding is separated with <c>,</c> and the keys within each keybind are separated with <c>+</c>. The key order is commutative, so <c>LeftControl + S</c> and <c>S + LeftControl</c> are identical.</remarks>
    public override string ToString()
    {
        return this.Keybinds.Length > 0
            ? string.Join(", ", this.Keybinds.Select(p => p.ToString()))
            : SButton.None.ToString();
    }
}
